import { Selector, SelectorType, AttributeAction } from "./types";

const attribValueChars = ["\\", '"'];
const pseudoValueChars = [...attribValueChars, "(", ")"];

const charsToEscapeInAttributeValue = new Set(
    attribValueChars.map((c) => c.charCodeAt(0)),
);
const charsToEscapeInPseudoValue = new Set(
    pseudoValueChars.map((c) => c.charCodeAt(0)),
);
const charsToEscapeInName = new Set(
    [
        ...pseudoValueChars,
        "~",
        "^",
        "$",
        "*",
        "+",
        "!",
        "|",
        ":",
        "[",
        "]",
        " ",
        ".",
        "%",
    ].map((c) => c.charCodeAt(0)),
);

/**
 * Turns `selector` back into a string.
 *
 * @param selector Selector to stringify.
 */
export function stringify(selector: Selector[][]): string {
    return selector
        .map((token) =>
            token
                .map((token, index, array) =>
                    stringifyToken(token, index, array),
                )
                .join(""),
        )
        .join(", ");
}

function stringifyToken(
    token: Selector,
    index: number,
    array: Selector[],
): string {
    switch (token.type) {
        // Simple types
        case SelectorType.Child: {
            return index === 0 ? "> " : " > ";
        }
        case SelectorType.Parent: {
            return index === 0 ? "< " : " < ";
        }
        case SelectorType.Sibling: {
            return index === 0 ? "~ " : " ~ ";
        }
        case SelectorType.Adjacent: {
            return index === 0 ? "+ " : " + ";
        }
        case SelectorType.Descendant: {
            return " ";
        }
        case SelectorType.ColumnCombinator: {
            return index === 0 ? "|| " : " || ";
        }
        case SelectorType.Universal: {
            // Return an empty string if the selector isn't needed.
            return token.namespace === "*" &&
                index + 1 < array.length &&
                "name" in array[index + 1]
                ? ""
                : `${getNamespace(token.namespace)}*`;
        }

        case SelectorType.Tag: {
            return getNamespacedName(token);
        }

        case SelectorType.PseudoElement: {
            return `::${escapeName(token.name, charsToEscapeInName)}${
                token.data === null
                    ? ""
                    : `(${escapeName(token.data, charsToEscapeInPseudoValue)})`
            }`;
        }

        case SelectorType.Pseudo: {
            return `:${escapeName(token.name, charsToEscapeInName)}${
                token.data === null
                    ? ""
                    : `(${
                          typeof token.data === "string"
                              ? escapeName(
                                    token.data,
                                    charsToEscapeInPseudoValue,
                                )
                              : stringify(token.data)
                      })`
            }`;
        }

        case SelectorType.Attribute: {
            if (
                token.name === "id" &&
                token.action === AttributeAction.Equals &&
                token.ignoreCase === "quirks" &&
                !token.namespace
            ) {
                return `#${escapeName(token.value, charsToEscapeInName)}`;
            }
            if (
                token.name === "class" &&
                token.action === AttributeAction.Element &&
                token.ignoreCase === "quirks" &&
                !token.namespace
            ) {
                return `.${escapeName(token.value, charsToEscapeInName)}`;
            }

            const name = getNamespacedName(token);

            if (token.action === AttributeAction.Exists) {
                return `[${name}]`;
            }

            return `[${name}${getActionValue(token.action)}="${escapeName(
                token.value,
                charsToEscapeInAttributeValue,
            )}"${
                token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"
            }]`;
        }
    }
}

function getActionValue(action: AttributeAction): string {
    switch (action) {
        case AttributeAction.Equals: {
            return "";
        }
        case AttributeAction.Element: {
            return "~";
        }
        case AttributeAction.Start: {
            return "^";
        }
        case AttributeAction.End: {
            return "$";
        }
        case AttributeAction.Any: {
            return "*";
        }
        case AttributeAction.Not: {
            return "!";
        }
        case AttributeAction.Hyphen: {
            return "|";
        }
        default: {
            throw new Error("Shouldn't be here");
        }
    }
}

function getNamespacedName(token: {
    name: string;
    namespace: string | null;
}): string {
    return `${getNamespace(token.namespace)}${escapeName(
        token.name,
        charsToEscapeInName,
    )}`;
}

function getNamespace(namespace: string | null): string {
    return namespace === null
        ? ""
        : `${
              namespace === "*"
                  ? "*"
                  : escapeName(namespace, charsToEscapeInName)
          }|`;
}

function escapeName(name: string, charsToEscape: Set<number>): string {
    let lastIndex = 0;
    let escapedName = "";

    for (let index = 0; index < name.length; index++) {
        if (charsToEscape.has(name.charCodeAt(index))) {
            escapedName += `${name.slice(lastIndex, index)}\\${name.charAt(index)}`;
            lastIndex = index + 1;
        }
    }

    return escapedName.length > 0 ? escapedName + name.slice(lastIndex) : name;
}
